# coding = utf-8
# File date: Hi_2022/11/7 22:46
# File_name: GUI_main.py
from mouse_package import mouse_method
from keyboard_package import keyboard_method
from tool_method import *
from threading import Thread
import time
import pynput
from pynput.mouse import Button
from pynput.keyboard import Key
from tkinter import Tk, BooleanVar
from tkinter.ttk import Notebook, Button as tk_Button, Label, Frame, Checkbutton
from tkinter.scrolledtext import ScrolledText

# 键盘按下时间
global last_events_time

# 日志初始化
g_log = log_out()
"""
logger.trace("'trace'级别日志,严重度值5")
logger.debug("'debug'级别日志,严重度值10")
logger.info("'info'级别日志,严重度值20")
logger.success("'success'级别日志,严重度值25")
logger.warning("'warning'级别日志,严重度值30")
logger.error("'error'级别日志,严重度值40")
logger.critical("'critical'级别日志,严重度值50")
"""


class RecorClicker(mouse_method, keyboard_method):
    def __init__(self, master):
        mouse_method.__init__(self)
        keyboard_method.__init__(self)
        # 创建record目录
        if not os.path.exists("./record"):
            os.mkdir("./record")
            g_log.success("record目录不存在已经自动创建")

        self.root = master

        self.record_status = False  # 录制状态
        self.mouse_track = BooleanVar(value=False)  # 鼠标轨迹录制状态

        self.play_status = False  # 播放状态
        self.play_cycle = BooleanVar(value=True)  # 循环播放状态

        g_log.success(f"初始化,设置录制状态record_status为{self.record_status}")
        g_log.success(f"初始化,设置鼠标轨迹录制状态mouse_track为{self.mouse_track.get()}")
        g_log.success(f"初始化,设置播放状态play_status为{self.play_status}")
        g_log.success(f"初始化,设置回放循环状态play_cycle为{self.play_cycle.get()}")

        # 初始化界面
        self.notebook = Notebook(self.root)  # 创建Notebook
        self.record_frame = Frame(self.notebook)  # 创建Frame1
        self.play_frame = Frame(self.notebook)  # 创建Frame2

        self.notebook.add(self.record_frame, text=f"{' ' * 17}录 制{' ' * 17}")  # 创建选项卡1同时插入Frame1
        self.notebook.add(self.play_frame, text=f"{' ' * 17}回 放{' ' * 17}")  # 创建选项卡2同时插入Frame2
        self.notebook.place(relx=0.005, rely=0, relwidth=0.995, relheight=1)

        # 两个界面布局初始化
        self.gui_layout()

        # 窗口关闭回调
        self.root.protocol('WM_DELETE_WINDOW', self.root_close)

        # 主循环
        g_log.success("开启主循环")
        self.root.mainloop()

    def gui_layout(self):
        """
        两个界面布局初始化
        """
        # ----------------------------------录像界面----------------------------------
        # 录像界面按钮和多行文本框
        record_label = Label(self.record_frame, text="", anchor="w")
        record_start = tk_Button(self.record_frame, text="开始/停止[F12]")
        mouse_track = Checkbutton(self.record_frame, text="鼠标轨迹", onvalue=True, offvalue=False, variable=self.mouse_track)
        record_scrolledtext = ScrolledText(self.record_frame, font=("黑体", 11))

        # 按钮方法绑定
        record_start.config(command=lambda: self.record_start(record_label, record_scrolledtext))
        mouse_track.config(command=lambda: self.cycle_track_switch("mouse_track"))

        # 录像界面布局
        record_start.place(relx=0.1, rely=0.013, relwidth=0.5, relheight=0.07)
        mouse_track.place(relx=0.63, rely=0.013, relwidth=0.3, relheight=0.07)
        record_label.place(relx=0, rely=0.09, relwidth=0.99, relheight=0.07)
        record_scrolledtext.place(relx=0, rely=0.17, relwidth=0.99, relheight=0.832)

        g_log.success("录像界面布局完成")
        # ----------------------------------回放界面----------------------------------
        # 回放界面按钮和多行文本狂
        play_label = Label(self.play_frame, text="", anchor="w")
        play_start = tk_Button(self.play_frame, text="开始/停止[F10]")
        play_save = tk_Button(self.play_frame, text="保存")
        play_open = tk_Button(self.play_frame, text="打开")
        play_scrolledtext = ScrolledText(self.play_frame, font=("黑体", 11))
        play_cycle = Checkbutton(self.play_frame, text="循环", onvalue=True, offvalue=False, variable=self.play_cycle)

        # 按钮方法绑定
        play_start.config(command=lambda: self.play_start(play_label, play_scrolledtext))
        play_open.config(command=lambda: write_to_text(play_label, play_scrolledtext))
        play_save.config(command=lambda: write_to_txt(play_label, play_scrolledtext))
        play_cycle.config(command=lambda: self.cycle_track_switch("play_cycle"))

        # 回放界面布局
        play_start.place(relx=0.05, rely=0.013, relwidth=0.28, relheight=0.07)
        play_save.place(relx=0.38, rely=0.013, relwidth=0.15, relheight=0.07)
        play_open.place(relx=0.58, rely=0.013, relwidth=0.15, relheight=0.07)
        play_cycle.place(relx=0.77, rely=0.013, relwidth=0.15, relheight=0.07)
        play_label.place(relx=0, rely=0.09, relwidth=0.99, relheight=0.07)
        play_scrolledtext.place(relx=0, rely=0.17, relwidth=0.99, relheight=0.832)

        g_log.success("回放界面布局完成")
        # ----------------------------------键盘监控----------------------------------
        # 开启键盘监控,键盘进程长留守护主进程
        args = (record_label, record_scrolledtext, play_label, play_scrolledtext)
        listener_keyboard_thread = Thread(target=self.keyboard_listener, args=args)
        listener_keyboard_thread.setDaemon(True)
        listener_keyboard_thread.start()
        g_log.success("开启键盘监控")

    def record_start(self, record_label: Label, record_scrolled_text: ScrolledText):
        """
        :param record_label: 需要显示保存路径的label
        :param record_scrolled_text: 多行文本框
        :return: 无返回,开始/停止录制
        """
        # 检测是否处于播放界面
        if self.notebook.select() == ".!notebook.!frame2":
            messagebox.showwarning(title="当前处于回放界面", message="当前处于回放界面,请先切换到录制界面")
            g_log.warning("当前处于回放界面,请先切换到录制界面")
            return

        # 当录制状态为False时,开启录制
        if not self.record_status:  # 检测是否处于播放模式
            # 防止反复开始录制先清空text
            record_scrolled_text.delete(0.0, END)
            g_log.success("滚动文本框内容清理")

            # 更新录制状态
            self.record_status = True
            self.play_status = False
            g_log.success("录制状态,回放状态更新")

            # 创建录像文件
            time.sleep(1)
            creat_record_file(record_label)
            g_log.success(f"创建录像文件：{record_label['text']}")

            # 开始记录上一次操作时间
            global last_events_time
            last_events_time = get_time()
            g_log.success(f"初始化last_events_time：{last_events_time}")
            g_log.success(f"当前鼠标轨迹录制状态mouse_track为{self.mouse_track.get()}")

            # 单独进程,监控鼠标
            mouse_listener_thread = Thread(target=self.mouse_listener, args=(record_scrolled_text,))
            mouse_listener_thread.setDaemon(True)
            mouse_listener_thread.start()
            g_log.success("鼠标监控线程已开启")

        # 当录制状态为True时,结束录制
        elif self.record_status:
            self.record_status = False
            g_log.success(f"更新record_status状态,停止录像")

            # 结束自动保存录像
            write_to_txt(record_label, record_scrolled_text)
            g_log.success("录制停止,更新record_status为False,在点点试试看")

    def play_start(self, play_label, play_scrolledtext):
        """
        开始停止播放点击回调
        """
        # 检测是否处于播放界面
        if self.notebook.select() == ".!notebook.!frame":
            messagebox.showwarning(title="当前处于录制界面", message="当前处于录制界面,请先切换到回放界面")
            g_log.warning("当前处于录制界面,请先切换到回放界面")
            return

        # 检测是否已经打开录像文件
        if not play_label["text"]:
            messagebox.showwarning("未打开文件", "请先打开需要播放的录像文件")
            g_log.warning("请先打开需要播放的录像文件")
            return

        # 检测当前是否处于录制模式
        if not self.play_status:  # 开始播放
            if self.record_status:  # 检测是否处于播放模式
                messagebox.showwarning(title="当前处于录制模式", message="当前处于录制模式,请先按下F12结束录制")
                g_log.warning("当前处于录制模式,请先按下F12结束录制")
                return
            self.play_status = True
            g_log.success("回放状态play_status更新为True,开启回放")

            # 独立进程中进行播放
            play_video_thred = Thread(target=self.play_video, args=(play_scrolledtext,))
            play_video_thred.setDaemon(True)
            play_video_thred.start()
            g_log.success("开启独立进程中进行播放")

        elif self.play_status:  # 停止播放
            self.play_status = False
            g_log.success("回放状态play_status更新为False,播放已经强制停止")

    def mouse_listener(self, scrolled_text: ScrolledText):
        """鼠标监听"""

        def on_move(x, y):
            """
            :param x: 点击时的x坐标, 绝对坐标int类型
            :param y: 点击时的y坐标,绝对坐标int类型
            :return: 鼠标发生移动时触发
            """
            # 检测录制状态停止监控
            if not self.record_status:
                g_log.info(f"录制状态为False,鼠标移动到({x}, {y})不记录")
                g_log.success(f"record_status状态已经更新为False,mouse_listener.on_move返回False结束鼠标监控")
                return False

            if self.mouse_track.get():  # 如果鼠标轨迹开启则记录
                # 把鼠标移动事件转换成字符串插入滚动文本框
                global last_events_time
                self.write_mouse_move(scrolled_text, x, y, get_sleep_time(last_events_time))
                g_log.info(f"鼠标移动到({x}, {y}已记录) ")

                # 记录本次操作结束时间,计算下个动作的间隔时间
                last_events_time = get_time()
                g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

            elif not self.mouse_track.get():  # 未开启轨迹记录不记录鼠标移动
                g_log.info(f"鼠标移动至坐标({x}, {y}),滚动检测鼠标轨迹记录未开启不记录")

        def on_click(x, y, button: Button, pressed):
            """
            :param x: 点击时的x坐标, 绝对坐标int类型
            :param y: 点击时的y坐标,绝对坐标int类型
            :param button: 鼠标点击的按键pynput.mouse.Button类型
            :param pressed: 布尔类型,Ture代表按钮下,False代表释放按键
            :return: 点击时间触发该方法
            """
            # 检测录制状态停止监控
            if not self.record_status:
                g_log.info(f"录制状态已经更新为False,本次鼠标在({x}, {y})按下{button}不记录")
                g_log.success(f"record_status状态已经更新为False,mouse_listener.on_click返回False结束鼠标监控")
                return False

            global last_events_time
            # 鼠标按下事件
            if pressed:
                # 如果鼠标轨迹开启则直接记录
                if self.mouse_track.get():

                    g_log.info(f"检测鼠标按下动作记录前滚动文本框内容")
                    start_tetx = scrolled_text.get('0.0', END).replace("\n", "")
                    if start_tetx == "":  # 检测第一个鼠标按下动记录前滚动文本框是否为空
                        g_log.success("检测到鼠标按下动作记录前滚动文本框内容为空,准备提前插入一行移动命令")
                        x, y = self.mouse.position  # 获取当前鼠标的位置
                        g_log.success(f"获取鼠标当前坐标为({x}, {y})")
                        self.write_mouse_move(scrolled_text, x, y, 0)

                    # 把鼠标按下转换成字符串插入滚动文本框
                    self.write_mouse_press(scrolled_text, f"{button}", get_sleep_time(last_events_time))
                    g_log.info(f"在坐标({x}, {y})按下{button}已记录")

                    # 记录本次操作结束时间,计算下个动作的间隔时间
                    last_events_time = get_time()
                    g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

                # 未开启轨迹记录则先插入一条移动鼠标到点击点
                elif not self.mouse_track.get():
                    # 先插入一条移动到点击点的命令
                    self.write_mouse_move(scrolled_text, x, y, 0)

                    # 把鼠标按下转换成字符串插入滚动文本框
                    self.write_mouse_press(scrolled_text, f"{button}", get_sleep_time(last_events_time))
                    g_log.info(f"在坐标({x}, {y})按下{button}已记录")

                    # 记录本次操作结束时间,计算下个动作的间隔时间
                    last_events_time = get_time()
                    g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

            # 鼠标释放事件
            elif not pressed:
                # 如果鼠标轨迹开启则记录
                if self.mouse_track.get():
                    # 把鼠标松开转换成字符串插入滚动文本框
                    self.write_mouse_release(scrolled_text, f"{button}", get_sleep_time(last_events_time))
                    g_log.info(f"鼠标{button}键在({x}, {y})松开已记录")

                    # 记录本次操作结束时间,计算下个动作的间隔时间
                    last_events_time = get_time()
                    g_log.info(f"更新last_events_time已记录：{last_events_time})")

                # 未开启轨迹记录则先插入一条移动鼠标到点击点
                elif not self.mouse_track.get():
                    # 先插入一条移动到点击点的命令
                    self.write_mouse_move(scrolled_text, x, y, 0)

                    # 把鼠标松开转换成字符串插入滚动文本框
                    self.write_mouse_release(scrolled_text, f"{button}", get_sleep_time(last_events_time))
                    g_log.info(f"鼠标{button}键在({x}, {y})松开已记录")

                    # 记录本次操作结束时间,计算下个动作的间隔时间
                    last_events_time = get_time()
                    g_log.info(f"更新last_events_time已记录：{last_events_time})")

        def on_scroll(x, y, dx, dy):
            """
            :param x: 点击时的x坐标, 绝对坐标int类型
            :param y: 点击时的y坐标,, 绝对坐标int类型
            :param dx: 同dy 应该是左右滚动int类型
            :param dy: 1向上,-1向下滚动int类型
            :return: 鼠标滚动时触发该方法
            """
            # 检测录制状态停止监控
            if not self.record_status:
                g_log.info(f"录制状态已经更新为False,鼠标在({x}, {y})点向{'上' if dy > 0 else '下'}滚动不记录")
                g_log.success(f"record_status状态已经更新为False,mouse_listener.on_scroll返回False结束鼠标监控")
                return False

            # 把鼠标滚动事件转换成字符串插入滚动文本框
            global last_events_time
            self.write_mouse_scroll(scrolled_text, dx, dy, get_sleep_time(last_events_time))
            g_log.info(f"鼠标在坐标({x}, {y})滚动已记录,dx：{dx}, dy:{dy}")
            # 记录本次操作结束时间,计算下个动作的间隔时间
            last_events_time = get_time()
            g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

        # 持续监控鼠标
        with pynput.mouse.Listener(on_move=on_move, on_click=on_click, on_scroll=on_scroll) as listener:
            listener.join()

    def keyboard_listener(self, record_label, record_scrolledtext, play_label, play_scrolledtext):
        """键盘监听"""

        def on_press(key: Key):
            """
            :param key: 按下的按键pynput.keyboard.Key
            :return: 按下按键时调用该方法
            """
            if not self.record_status:  # 如果录制未开启忽略所有按下
                g_log.info(f"未开启录制,不记录按下按键{key}")

            elif self.record_status:  # 如果录制状态开启
                if key == Key.f12 or key == Key.f10:  # 如果按下的是f10或f12则忽略
                    g_log.info(f"此时录制状态：{self.record_status},按下{key}忽略不记录")

                else:
                    start_tetx = record_scrolledtext.get('0.0', END).replace("\n", "")
                    g_log.success(f"检测键盘按下动作记录前滚动文本框中的内容")
                    if start_tetx == "":  # 检测第一个鼠标按下动记录前滚动文本框是否为空
                        g_log.warning("检测键盘按下动作记录前滚动文本框中的内容为空,准备提前插入一行移动命令")
                        x, y = self.mouse.position  # 获取当前鼠标的位置
                        g_log.success(f"获取鼠标当前坐标为({x}, {y})")
                        self.write_mouse_move(record_scrolledtext, x, y, 0)
                        g_log.success(f"修改第一行记录为移动到点击的点")

                    # 把键盘按下事件写入滚动文本框
                    global last_events_time
                    self.write_key_press(record_scrolledtext, f"{key}", get_sleep_time(last_events_time))
                    g_log.info(f"此时录制状态：{self.record_status},按下：{key}已记录")

                    # 记录本次操作结束时间,计算下个动作的间隔时间
                    last_events_time = get_time()
                    g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

        def on_release(key):
            """
            :param key: 按下的按键pynput.keyboard.Key
            :return: 松开按键时回调该方法
            """
            # 检测按键
            if key == pynput.keyboard.Key.f12:  # 释放F12时
                g_log.success("按下F12")
                if not self.record_status:  # 释放F12开始录制
                    # 这里不改状态 调用record_start修改了
                    g_log.success(f"检测到释放F12,尝试开始录制,并停止播放")
                    self.record_start(record_label, record_scrolledtext)
                    g_log.success("调用record_start开始录制")

                elif self.record_status:  # 释放F12停止录制
                    self.record_start(record_label, record_scrolledtext)
                    g_log.success("调用record_start,停止录制")

            elif key == pynput.keyboard.Key.f10:  # 释放F10时
                g_log.success(f"检测到释放F10")
                if not self.play_status:  # 释放F10开始播放
                    self.play_start(play_label, play_scrolledtext)
                    g_log.success(f"检测到释放F10,尝试开始播放,并停止录像")

                elif self.play_status:  # 释放F10停止播放
                    self.play_start(play_label, play_scrolledtext)
                    g_log.success(f"调用play_start, 停止播放")

                else:
                    g_log.error(f"键盘监控,释放F10后遇到未知错误")

            elif not self.record_status:  # 如果录制状态未开启,则忽略
                g_log.success(f"录制状态关闭,不记录按键{key}释放")

            else:  # 当释放其他按键时,写入
                # 把键盘释放事件写入滚动文本框
                global last_events_time
                self.write_key_release(record_scrolledtext, f"{key}", get_sleep_time(last_events_time))
                g_log.info(f"此时录制状态：{self.record_status},释放：{key}")

                # 记录本次操作结束时间,计算下个动作的间隔时间
                last_events_time = get_time()
                g_log.info(f"操作结束更新last_events_time已记录：{last_events_time})")

        # 注册系统全局热键监控键盘,监控按下F10
        with pynput.keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
            listener.join()

    def play_video(self, play_scrolledtext: ScrolledText):
        """
        :param play_scrolledtext: 要播放录像的滚动文本框
        :return: 1秒后自动进行播放
        """
        g_log.success(f"1秒后开始播放录像")
        time.sleep(1)

        # 提取出所有字符串
        all_str = play_scrolledtext.get(0.0, END).splitlines()
        g_log.success("滚动文本框所有字符串已提取")

        # 提取出文本行数
        all_lines = len(all_str)
        g_log.success(f"all_lines：{all_lines}")

        g_log.success("检查是否循环执行")
        if self.play_cycle.get():
            g_log.success("play_cycle为True,循环执行直至强制播放停止")
            times = 1
            while self.play_cycle.get():
                if not self.play_status:
                    g_log.warning("play_status录制状态已经更新,退出循环执行")
                    break
                g_log.success(f"第{times}轮循环开始执行")
                self.play_analysis(play_scrolledtext, all_lines, all_str)
                g_log.success(f"第{times}轮循环执行完成")
                times += 1

        else:
            g_log.success("play_cycle为False,只执行一轮")
            self.play_analysis(play_scrolledtext, all_lines, all_str)

        # 执行完成回放状态改回False
        self.play_status = False
        g_log.success("执行完成play_status状态更新为False")

    def play_analysis(self, play_scrolledtext: ScrolledText, all_lines, all_str):
        """执行滚动文本框中的字符串"""
        # 遍历每行并执行
        for line_num, line in zip(range(all_lines), all_str):
            if not self.play_status:  # 强制停止
                g_log.warning(f"正在回放时,回放状态更新,强制结束回放")
                break
            if line == "":  # 遇到空行跳过
                g_log.warning(f"第{line_num + 1}行为空行跳过执行")
                continue
            else:
                # 给每一行补上self.
                line = f"self.{line}"
                g_log.info(f"本行待执行文本处理后:{line}")

            # 给执行的内容添加tag、mark,标红加下划线
            line_lenth = len(line)  # 执行行长度
            g_log.info(f"本行待执行文本长度:{line_lenth}")
            line_index1 = f"{line_num}.0"  # 设定tag起始位置
            line_index2 = f"{line_num}.{line_lenth}"  # 设定tag结束位置
            play_scrolledtext.tag_add("in_progress", line_index1, line_index2)  # 设置tag
            play_scrolledtext.tag_config("in_progress", foreground="red", underline=True)
            g_log.info("本行已mark为tag,并设置下划线 前景为red")

            # 设置mark,当到最底部时,每次下移一行
            play_scrolledtext.mark_set("in_progress_mark", f"{line_num + 1}.0")  # 设置mark
            play_scrolledtext.see("in_progress_mark")  # 移动光标到mark
            g_log.info(f"设置mark成功,焦点移动到第{line_num}行")

            # 设置mark,当到最底部时,每次下移多行
            # if line_num % 20 == 0:
            #     play_scrolledtext.mark_set("in_progress_mark", f"{line_num + 27}.0")  # 设置mark
            #     play_scrolledtext.see("in_progress_mark")  # 移动光标到mark
            #     g_log.info("设置mark成功,焦点移动到距离mark处{27}行")

            # 执行
            try:
                g_log.info(f"准备执行内容:{line}")
                eval(line)
            except (SyntaxError, AttributeError, NameError, BaseException):
                g_log.error(f"回放文件内容被篡改,回放到第{line_num + 1}行发现错误,无法继续,已结束回放")
                self.play_status = False
                play_scrolledtext.tag_delete("in_progress")  # 报错删除tag
                play_scrolledtext.mark_unset("in_progress_mark")  # 报错删除mark
                g_log.error("遇到错误,停止执行并已有删除tag和mark")
                messagebox.showerror(title="录像文件损坏", message=f"回放文件内容被篡改,回放到第{line_num + 1}行发现错误,无法继续,已结束回放")
                break

            play_scrolledtext.tag_delete("in_progress")  # 执行完成删除tag
            play_scrolledtext.mark_unset("in_progress_mark")  # 下移一行,删除mark

            g_log.info("执行完成本行删除tag和mark")

            # 下移多行,删除mark
            # if line_num % 20 == 0:
            #     play_scrolledtext.mark_unset("in_progress_mark")  # 执行完成删除mark
            #     g_log.info("执行完成本行删除mark")

    def cycle_track_switch(self, switch: str):
        """
        :return: 无返回，记录修改后的循环状态或鼠标轨迹记录状态
        """
        if switch == "play_cycle":  # 播放循环状态修改回调
            g_log.success(f"{switch}状态更新为{self.play_cycle.get()}")
        elif switch == "mouse_track":  # 鼠标轨迹记录状态修改回调
            g_log.success(f"{switch}状态更新为{self.mouse_track.get()}")

    def root_close(self):
        """关闭回调"""
        # 更新状态
        g_log.critical(f"准备结束进程更新record_status和play_status状态")

        self.record_status = False
        self.play_status = False

        g_log.critical("杀死所有子线程和组件")
        # 关闭窗口,杀死所有子组件
        self.root.destroy()


if __name__ == '__main__':
    # 主窗口
    root = Tk()
    g_log.success("窗口初始化")

    # 窗口标题
    root.title("Auto Clicker")
    g_log.success(f"设置窗口标题")

    # 桌面长宽
    win_w = root.winfo_screenwidth()
    win_h = root.winfo_screenheight()

    # 窗口长宽
    width = 350
    height = 450

    # 设置窗口位置和长宽
    root.geometry(f"{width}x{height}+{(win_w - width) // 2}+{(win_h - height) // 2}")
    g_log.success(f"设置窗口宽高{width}x{height}并居中")

    # 不可调整大小
    root.resizable(False, False)
    g_log.success("设置窗口不可调节大小")

    # 窗口置顶
    root.wm_attributes('-topmost', 1)
    g_log.success("设置窗口置顶")

    RecorClicker(root)
